【Python】マッチ棒クイズ開発 Part20

こんにちは、にわこまです。

今回は、pythonファイルとkvファイルを使ってマッチ棒枠とマッチ棒を動かす画面を作成します。マッチ棒を動かす画面とはPlayFieldWidgetのことです。

 

誤字脱字や分からない点がございましたらご連絡お願いいたします!

メールまたはTwitterのDMまで!!

 

 

 

スポンサードサーチ


マッチ棒枠

マッチ棒枠はマッチ棒を移動先となるものです。薄いグレーでマッチ棒を表現したクラスです。マッチ棒と違い関数は必要ありません。

基本的は定義は全てkvファイルに記述します。

 

 

コードの提示

kvファイルでマッチ棒枠クラスを表現するソースコードを以下に示します。

# -*- coding: utf-8 -*-
#:import np numpy

<MatchStickWidget>:
    # 省略

<MatchStickFrameWidget>:
    size: [0, 0]
    ms_w: 25
    angle: 0
    canvas:
        PushMatrix:
        Rotate:
            group: "rot"
            angle: self.angle
            origin: (self.x + self.ms_w / 2, self.y + self.ms_w * 6.8 / 2)
        Color:
            rgb: [0.95, 0.95, 0.95]
        Rectangle:
            pos: [self.x+self.ms_w*0.2, self.y]
            size: [self.ms_w * 0.6, self.ms_w * 5.6]
        Ellipse:
            pos: [self.x, self.y+self.ms_w * 5.2]
            size: [self.ms_w, self.ms_w * 1.6]
        PopMatrix:


<RootWidget>:
    canvas:
        Color:
            rgb: [1, 1, 1]
        Rectangle:
            pos: self.pos
            size: self.size

 

pythonファイルでマッチ棒枠クラスを表現するソースコードを以下に示します。

import kivy
kivy.require('2.0.0')

from kivy.app import App
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.widget import Widget

from kivy.lang import Builder
Builder.load_file('matchstickquiz_V2001.kv')

import numpy as np

class MatchStickWidget(Widget):
    # 省略

class MatchStickFrameWidget(Widget):
    def __init__(self, ms_w, angle, **kwargs):
        super(MatchStickFrameWidget, self).__init__(**kwargs)
        self.ms_w = ms_w
        self.angle = angle
        self.rot = self.canvas.get_group("rot")[0]

class RootWidget(FloatLayout):
    pass


class MatchStickQuizApp(App):
    def build(self):
        root = RootWidget()
        root.add_widget(MatchStickFrameWidget(25, 0))
        return root


if __name__ == '__main__':
    MatchStickQuizApp().run()
    pass

 

上記のコードは以下からダウンロードできます。

  

 

コードの解説 kvファイル

7行目から25行目の「MatchStickFrameWidget」は、マッチ棒枠を表現するクラスです。

 

8行目の「size」は、マッチ棒枠のサイズを保持する変数です。横幅縦幅それぞれ0を代入しています。

 

9行目の「ms_w」は、マッチ棒枠の横幅を保持する変数です。初期値として25を代入しています。

 

10行目の「angle」は、マッチ棒枠の角度を保持する変数です。初期値として0を代入しています。

 

11行目の「canvas」は、canvasオブジェクトを宣言しています。この宣言以降でインデントを1つ下げるとcanvasへ追加するという扱いになります。

 

12行目の「PushMatrix:」は、回転する範囲の始まりを宣言しています。後述するPopMatrixとで囲まれたオブジェクトは一緒に回転します。

 

13行目の「Rotate」は、回転するために必要なオブジェクトです。角度や回転の中心を設定します。

14行目の「group: ‘rot’」は、Rotateオブジェクトにrotグループという属性を付与しています。pythonファイル側からRotateオブジェクトを呼び出す時に使用します。

15行目の「angle」は、角度を保持する変数です。

16行目の「origin」は、回転の中心を保持位する変数です。計算はPart7で解説しています。

 

17行目の「Color」は、後述するRectangleとEllipseの色を設定するオブジェクトです。

18行目では、色を薄いグレーに設定しています。

 

19行目の「Rectangle」は、マッチ棒枠の棒部分を表現するオブジェクトです。

20行目の「pos」は、マッチ棒の棒部分の座標を保持する変数です。

21行目の「size」は、マッチ棒の棒部分のサイズを保持する変数です。

 

22行目の「Ellipse」は、マッチ棒の火薬部分を表現するオブジェクトです。

23行目の「pos」は、マッチ棒の火薬部分の座標を保持する変数です。

24行目の「size」は、マッチ棒の火薬部分のサイズを保持する変数です。

 

25行目の「PopMatrix」は、回転する範囲の終わりを宣言しています。前述したPushMatrixとで囲まれたオブジェクトは一緒に回転します。

 

 

コードの解説 pythonファイル

pythonに書かれているコードは、ほとんどこれまでのPartで解説しているため、新しく記述したコードや気を付けて欲しいコードについて解説します。

 

16行目から21行目の「MatchStickFrameWidgetクラス」は、マッチ棒枠を表現するクラスです。

 

17行目から21行目の「init関数」は、マッチ棒枠を初期化しています。

 

18行目の「super関数」は、Widgetクラスのinit関数を実行しています。

 

19行目の「ms_w」は、マッチ棒の横幅を保持する変数です。kvファイルにて初期値を25としていますが、こちらに代入した値が優先されます。

 

20行目の「self.angle」は、マッチ棒の角度を保持する変数です。kvファイルにて初期値を0としていますが、こちらに代入した値が優先されます。

 

21行目の「self.rot = self.canvas.get_group(“rot”)[0]」は、self.rotにkvファイルで宣言したRotateオブジェクトを代入しています。

get_group関数はcanvas上のオブジェクトでrotというグループが付与されている全てのオブジェクトをリストで取得しています。

rotというグループが付与されているオブジェクトはRotateしかないため、0番目を指定してRotateオブジェクトを取得しています。

 

 

動作確認

コードを保存したら、コマンドプロンプトを開きファイルを保存したフォルダまで移動します。移動したら、以下のコマンドを入力し実行します。

python matchstickquiz.py

 

以下のように左下に薄いグレーでマッチ棒が表示されていれば動作確認完了です。

 

 

スポンサードサーチ


マッチ棒を動かす画面

ここでは、マッチ棒枠を表示したりマッチ棒を問題の通りに表示したりします。

以下の画像における薄い赤色の範囲を作成します。

 

 

コードの提示

kvファイルでマッチ棒枠クラスを表現するソースコードを以下に示します。

# -*- coding: utf-8 -*-
#:import np numpy

<MatchStickWidget>:
    # 省略

<MatchStickFrameWidget>:
    # 省略

<PlayFieldWidget>:
    ms_w: 25
    ms_h: self.ms_w * 6.8
    base_x: self.center_x - (45 * self.ms_w) / 2
    base_y: self.center_y - (17 * self.ms_w) / 2
    frameList: []
    msList: []
    msDict: {}
    qaDict: {}
    quesList: []
    ansList: []
    input_ansList: []
    on_size: self.drawFrame(), self.drawQuestion()
    canvas:
        Color:
            rgb: [1, 1, 1]
        Rectangle:
            pos: self.pos
            size: self.size


<RootWidget>:
    canvas:
        Color:
            rgb: [1, 1, 1]
        Rectangle:
            pos: self.pos
            size: self.size

  

pythonファイルでマッチ棒枠クラスを表現するソースコードを以下に示します。

import kivy
kivy.require('2.0.0')

from kivy.app import App
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.widget import Widget
from kivy.storage.jsonstore import JsonStore

from kivy.lang import Builder
Builder.load_file('matchstickquiz.kv')

import numpy as np
import random
import copy

class MatchStickWidget(Widget):
    # 省略

class MatchStickFrameWidget(Widget):
    # 省略

class PlayFieldWidget(FloatLayout):
    def __init__(self, **kwargs):
        super(PlayFieldWidget, self).__init__(**kwargs)
        self.onMoveObj = None
    def setQA(self, *args):
        for ms in list(self.msDict.keys()):
            self.remove_widget(ms)
        self.msDict = {}
        self.onMoveObj = None
        ans = random.choice(list(self.QADict.keys()))
        self.quesList = random.choice(self.QADict[ans]).split(" ")
        self.ansList = ans.split(" ")
        self.input_ansList = []
        self.drawQuestion()
    def calcPos(self, base_x, base_y, k, *args):
        # base (xが若い順 -> yが若い順)
        base_num_pos = [(base_x, base_y+self.ms_w, 0), (base_x, base_y+self.ms_w+self.ms_h+self.ms_w, 0),
                        (base_x+self.ms_w+self.ms_h/2-self.ms_w/2, base_y-self.ms_h/2+self.ms_w/2, 90),
                        (base_x+self.ms_w+self.ms_h/2-self.ms_w/2, base_y+self.ms_w+self.ms_h+self.ms_w/2-self.ms_h/2, 90),
                        (base_x+self.ms_w+self.ms_h/2-self.ms_w/2, base_y+self.ms_w+self.ms_h+self.ms_w+self.ms_h+self.ms_w/2-self.ms_h/2, 90),
                        (base_x+self.ms_w+self.ms_h, base_y+self.ms_w, 0), (base_x+self.ms_w+self.ms_h, base_y+self.ms_w+self.ms_h+self.ms_w, 0)]
        # sign
        plus_sign_pos = [(base_x+self.ms_w+self.ms_h/2-self.ms_w/2, base_y+self.ms_w+self.ms_h+self.ms_w/2-self.ms_h/2, 0),
                         (base_x+self.ms_w+self.ms_h/2-self.ms_w/2, base_y+self.ms_w+self.ms_h+self.ms_w/2-self.ms_h/2, 90)]
        minus_sign_pos = [(base_x+self.ms_w+self.ms_h/2-self.ms_w/2, base_y+self.ms_w+self.ms_h+self.ms_w/2-self.ms_h/2, 90)]
        equal_sign_pos = [(base_x+self.ms_w+self.ms_h/2-self.ms_w/2, base_y+self.ms_w+self.ms_h-(self.ms_h/2-self.ms_w)/2-self.ms_h/2, 90),
                          (base_x+self.ms_w+self.ms_h/2-self.ms_w/2, base_y+self.ms_w+self.ms_h+self.ms_w+(self.ms_h/2-self.ms_w)/2-self.ms_h/2, 90)]
        # number
        zero_num_pos  = base_num_pos[0:3] + base_num_pos[4:]
        one_num_pos   = base_num_pos[5:]
        two_num_pos   = [base_num_pos[i] for i in [0, 2, 3, 4, 6]]
        three_num_pos = base_num_pos[2:]
        four_num_pos  = [base_num_pos[i] for i in [1, 3, 5, 6]]
        five_num_pos  = base_num_pos[1:6]
        six_num_pos   = base_num_pos[:-1]
        seven_num_pos = [base_num_pos[1]] + base_num_pos[4:]
        eight_num_pos = base_num_pos
        nine_num_pos  = base_num_pos[1:]
        pos_dict = {"base":base_num_pos, "+":plus_sign_pos, "-":minus_sign_pos, "=":equal_sign_pos,
                    "0":zero_num_pos, "1":one_num_pos, "2":two_num_pos, "3":three_num_pos, "4":four_num_pos,
                    "5":five_num_pos, "6":six_num_pos, "7":seven_num_pos, "8":eight_num_pos, "9":nine_num_pos}
        return pos_dict[k]
    def drawFrame(self, *args):
        if(len(self.frameList) == 0):
            for idx, k in enumerate(["base", "+", "base", "=", "base"]):
                pos_list = self.calcPos(self.base_x+idx*(self.ms_w+self.ms_h+self.ms_w), self.base_y, k)
                for pos_t in pos_list:
                    obj = MatchStickFrameWidget(pos=[pos_t[0], pos_t[1]], ms_w=self.ms_w, angle=pos_t[2])
                    self.frameList.append(obj)
                    self.add_widget(obj)
        elif(len(self.frameList) != 0):
            c = 0 # index of self.frameList
            for idx, k in enumerate(["base", "+", "base", "=", "base"]):
                pos_list = self.calcPos(self.base_x+idx*(self.ms_w+self.ms_h+self.ms_w), self.base_y, k)
                for pos_t in pos_list:
                    obj = self.frameList[c]
                    obj.ms_w = self.ms_w
                    obj.pos = [pos_t[0], pos_t[1]]
                    obj.angle = pos_t[2]
                    c += 1
    def drawQuestion(self, *args):
        if(len(self.msDict) == 0):
            for idx, k in enumerate(self.quesList):
                pos_list = self.calcPos(self.base_x+idx*(self.ms_w+self.ms_h+self.ms_w), self.base_y, k)
                for pos_t in pos_list:
                    obj = MatchStickWidget(pos=[pos_t[0], pos_t[1]], ms_w=self.ms_w, angle=pos_t[2])
                    self.msDict[obj] = ([pos_t[0], pos_t[1]], pos_t[0], pos_t[1], pos_t[2])
                    self.add_widget(obj)
        elif(len(self.msDict) != 0):
            tempmsList = list(self.msDict.keys())
            c = 0       # index of tempmsList
            for idx, k in enumerate(self.quesList):
                pos_list = self.calcPos(self.base_x+idx*(self.ms_w+self.ms_h+self.ms_w), self.base_y, k)
                for pos_t in pos_list:
                    obj = tempmsList[c]
                    obj.ms_w = self.ms_w
                    obj.pos = [pos_t[0], pos_t[1]]
                    obj.angle = pos_t[2]
                    self.msDict[obj] = ([pos_t[0], pos_t[1]], pos_t[0], pos_t[1], pos_t[2])
                    c += 1
    def drawAnswer(self, *args):
        # 「〇 + ◇ = △」と「〇 - ◇ = △」の場合のみを考える
        tempmsList = list(self.msDict.keys())
        c = 0       # index of tempmsList
        for idx, k in enumerate(self.ansList):
            pos_list = self.calcPos(self.base_x+idx*(self.ms_w+self.ms_h+self.ms_w), self.base_y, k)
            for pos_t in pos_list:
                obj = tempmsList[c]
                obj.pos = [pos_t[0], pos_t[1]]
                obj.angle = pos_t[2]
                self.msDict[obj] = ([pos_t[0], pos_t[1]], pos_t[0], pos_t[1], pos_t[2])
                c += 1
    def checkAnswerFML(self, *args):
        posTupleList = sorted([ms.pos for ms in list(self.msDict.keys())])
        self.input_ansList = []
        for idx, k in enumerate(self.ansList):
            pos_x_min = self.base_x+idx*(self.ms_w+self.ms_h+self.ms_w)
            pos_x_max = self.base_x+(idx+1)*(self.ms_w+self.ms_h+self.ms_w)
            posList = [t for t in posTupleList if(pos_x_min <= t[0] and t[0] < pos_x_max)]
            lng_posList = len(posList)  # マッチ棒が何本使われているか数える
            if(lng_posList == 1 and posList == [[t[0], t[1]] for t in self.calcPos(pos_x_min, self.base_y, "-")]):
                self.input_ansList.append("-")
            elif(lng_posList == 2):
                for i in ["1", "+", "="]:
                    if(posList == [[t[0], t[1]] for t in self.calcPos(pos_x_min, self.base_y, i)]):
                        self.input_ansList.append(i)
            elif(lng_posList == 4):
                for i in ["4", "7"]:
                    if(posList == [[t[0], t[1]] for t in self.calcPos(pos_x_min, self.base_y, i)]):
                        self.input_ansList.append(i)
            elif(lng_posList == 5):
                for i in ["2", "3", "5"]:
                    if(posList == [[t[0], t[1]] for t in self.calcPos(pos_x_min, self.base_y, i)]):
                        self.input_ansList.append(i)
            elif(lng_posList == 6):
                for i in ["0", "6", "9"]:
                    if(posList == [[t[0], t[1]] for t in self.calcPos(pos_x_min, self.base_y, i)]):
                        self.input_ansList.append(i)
            elif(lng_posList == 7 and posList == [[t[0], t[1]] for t in self.calcPos(pos_x_min, self.base_y, "8")]):
                self.input_ansList.append("8")
        if(self.input_ansList == self.ansList):
            return True
        return False
    def checkOnMoveObj(self, obj, *args):
        tof_None = True if(self.onMoveObj == None) else False
        tof_obj = True if(self.onMoveObj == obj) else False
        tof_pos = True if(self.msDict[obj][0] == obj.pos) else False
        tof_angle = True if(self.msDict[obj][3] == obj.angle % 180) else False
        if(tof_None):
            self.onMoveObj = obj
        elif(not(tof_None) and not(tof_obj) and (not(tof_pos) or not(tof_angle))):
            obj.pos = self.msDict[obj][0]
            obj.angle = self.msDict[obj][3]
        elif(not(tof_None) and tof_obj and tof_pos and tof_angle):
            self.onMoveObj = None
    def on_touch_down(self, touch):
        for idx, obj in enumerate(self.children):
            tof = obj.on_touch_down(touch) if(obj == self.onMoveObj or self.onMoveObj == None) else False
            if(tof and idx != 0):
                # Here is bringing the clicked object to the foreground.
                self.remove_widget(obj)
                self.add_widget(obj)
                break
            elif(tof and idx == 0 and touch.is_double_tap):
                # Here is rotating the object of foreground.
                self.checkOnMoveObj(obj)
                break
    def on_touch_up(self, touch):
        if(len(self.children) != 0 and type(self.children[0]) == MatchStickWidget):
            obj = self.children[0]
            obj.on_touch_up(touch)
            tofList = list(map(obj.collide_center_circle, self.frameList)) # MatchStickFrameWidget and MatchStickWidget
            tof_obj_frame = True in tofList
            frame = self.frameList[tofList.index(True)] if(tof_obj_frame) else None
            tof_obj_obj = True in list(map(obj.collide_center_circle, list(self.msDict.keys())))
            if(tof_obj_obj):
                obj.pos = copy.copy(obj.before_move_pos)
            elif(not(tof_obj_obj) and tof_obj_frame):
                obj.pos = copy.copy(frame.pos)
                obj.before_move_pos = copy.copy(frame.pos)
            elif(not(tof_obj_obj) and not(tof_obj_frame)):
                obj.before_move_pos = copy.copy(obj.pos)
            self.checkOnMoveObj(obj)

class RootWidget(FloatLayout):
    pass


class MatchStickQuizApp(App):
    def build(self):
        root = RootWidget()
        pfw = PlayFieldWidget()
        pfw.drawFrame()
        pfw.QADict = JsonStore("./matchstickquiz_QA2.json")
        pfw.setQA()
        root.add_widget(pfw)


if __name__ == '__main__':
    MatchStickQuizApp().run()
    pass

  

上記のコードは以下からダウンロードできます。

  

 

コードの解説 kvファイル

10行目から28行目の「PlayFieldWidget」は、マッチ棒を動かす画面です。

 

11行目の「ms_w」は、マッチ棒の横幅を保持する変数です。25を代入しています。

 

12行目の「ms_h」は、マッチ棒の縦幅を保持する変数です。ms_wに6.8をかけた値を代入しています。

 

13行目の「base_x」と14行目の「base_y」は、問題となる式を真ん中に表示するための左下の座標です。基準となる座標を保持します。

 

15行目の「frameList」は、マッチ棒を動かす画面に追加されたマッチ棒枠を保持する変数です。

 

16行目の「msList」は、マッチ棒を動かす画面に追加されたマッチ棒を保持する変数です。

 

17行目の「msDict」は、キーにマッチ棒、値にマッチ棒の座標や角度を保持する変数です。

  

18行目の「qaDict」は、Jsonファイルから問題と答えを読み込んで保持する変数です。今までは、QADictという変数でしたがqaDictと変わりました。理由としましては、kvファイルでは大文字から始まる変数を定義することができないからです。

 

19行目の「quesList」は、現在表示している問題を保持する変数です。

 

20行目の「ansList」は、現在の問題の答えを保持する変数です。

 

21行目の「input_ansList」は、プレイヤーが入力した答えを保持する変数です。

 

22行目の「on_size」は、sizeに変化があった際に実行される関数です。pythonファイルで定義するdrawFrameとdrawQuestionを設定しています。sizeに変化があった際はdrawFrameとdrawQuestionが実行されます。

 

23行目の「canvas」は、canvasオブジェクトを宣言しています。この宣言以降でインデントを1つ下げるとcanvasへ追加するという扱いになります。

 

24行目の「Color」は、後述するRectangleの色を設定するオブジェクトです。

25行目では、色を白色に設定しています。

 

26行目の「Rectangle」は、背景を表現するオブジェクトです。

27行目の「pos」は、背景の座標を保持する変数です。

28行目の「size」は、背景のサイズを保持する変数です。

  

 

コードの解説 pythonファイル

pythonに書かれているコードは、ほとんどこれまでのPartで解説しているため、新しく記述したコードや気を付けて欲しいコードについて解説します。

 

25行目の「self.onMoveObj = None」は、kvファイルで定義してもよい変数ですが、Noneの代入が不可であるためpythonファイルで定義しています。

 

64行目から81行目の「drawFrame」は、今まで定義してきたdrawFrameとは若干異なります。

66行目から71行目の処理はinit関数に記述していた処理です。その処理をdrawFrame関数に追加した形となります。

また、init関数に記述していた処理は1回だけ実行して欲しいため、if文を追加しています。

  

 

動作確認

コードを保存したら、コマンドプロンプトを開きファイルを保存したフォルダまで移動します。移動したら、以下のコマンドを入力し実行します。

python matchstickquiz.py

 

以下のように問題となる式が表示されていれば動作確認完了です。問題はランダムに選択されるため式が異なっていても問題ありません。

 

 

スポンサードサーチ


まとめ

今回は、マッチ棒枠とマッチを動かす画面を作成しました。

マッチ棒を動かす画面では関数が必須であるため、pythonに記述するコードの量はそこまで変わらなかったと思います。

また、大文字から始まる変数を定義できないやNoneを代入できないというkvファイル特有の仕様がありました。

 

次回Part21では、プレイ画面の右側に表示するボタンの親クラスとポップアップを作成します。

 

 

  

最後までお読みいただきありがとうございます。


スポンサードサーチ