Python Tkinter GUIプログラミング 数字合わせ

 今日も見に来てくださって、ありがとうございます。石川さんです。

 今回紹介する数字合わせは、先日読んだ書籍に、作業記憶と注意力を鍛え、認知プロセスをスピードアップする練習として紹介されていました。練習を積むと、直観が信じられるようになるとか。作業記憶が鍛えられると一度見たものをもう一度確認しなくても済むようになるそうです。作業記憶と、注意力、鍛えたいですねぇ。

 実際は、トランプをつかってやるのですけど、せっかくtkinterでプログラムが作れるので、ちょっと作ってみることにしました。やり方は、神経衰弱と同じです。実行するとカードが4×3の12枚配られた状態になります。クリックするとマークが表示されます。2枚ずつクリックして、数字を一致させます。

できあがりイメージ

数字合わせ 4×3

ソースコード

from tkinter import Canvas, Tk, BOTH, ALL
from tkinter import messagebox
from math import sqrt, ceil, floor
from random import sample
from collections import namedtuple

Suit = namedtuple('Suit',['figure','color','text'])
Club = Suit("\u2663","#000000","Club")
Heart = Suit("\u2665","#FF0000","Heart")
Spade = Suit("\u2660","#000000","Spade")
Diamond = Suit("\u2666","#FF0000","Diamond")
Suits = [Club,Heart,Spade,Diamond]
Rank = ["A","2","3","4","5","6","7","8","9","10","J","Q","K"]
Card = namedtuple('Card',['suit','rank'])
N = 6

class NumberMaching(Tk):
    def __init__(self):
        super().__init__()
        self.title("Number Maching")
        self.state("zoomed")
        self.cards = [Card(suit, r) for r in Rank[:N] for suit in Suits[:2]]

        self.canvas = Canvas(self)
        self.canvas.pack(expand=True,fill=BOTH)
        self.refresh_cards()

    def refresh_cards(self):
        self.canvas.delete(ALL)
        w, h = self.winfo_screenwidth(), self.winfo_screenheight()
        width, height = ceil(sqrt(N*2)), floor(sqrt(N*2))
        if N*2 > width * height:
            height += 1
        wm, hm = w // (width*2), h // (height*2) # width margin, height margin
        self.q = sample(self.cards,len(self.cards))
        self.items = [None] * len(self.q)
        self.answers = []
        self.closing = False
        self.tapped = 0

        for i in range(height):
            for j in range(width):
                n = i*width + j
                if n >= N*2:
                    break
                figure = self.q[n].suit.figure + self.q[n].rank
                color = self.q[n].suit.color
                x = j*((w-wm)//width) + wm
                y = i*((h-wm)//height) + hm
                item = self.canvas.create_text(x,y,text=figure,fill=color,font=("",60),tags="card")
                rect = self.get_rectangle(x,y,(w-wm)//width*.9,(h-wm)//height*.9)
                carditem = self.canvas.create_rectangle(rect,fill="white",tags="card")
                self.items[n] = [item, carditem, self.q[n].rank] # text, rectangle, rank
        self.canvas.tag_bind("card","<Button-1>",self.card_tapped)

    def get_rectangle(self,center_x,center_y,width,height):
        leftx = center_x - width // 2
        topy = center_y - height // 2
        rightx = center_x + width // 2
        bottomy = center_y + height //2
        return (leftx,topy,rightx,bottomy)

    def card_tapped(self,event):
        if self.closing:
            return
        self.tapped += 1
        item = self.canvas.find_closest(event.x,event.y)
        for n, a in enumerate(self.items):
            if a[0] == item[0] or a[1] == item[0]:
                break
        if n > N * 2:
            return
        isopen = False
        for i in self.answers:
            if i == n:
                isopen = True
        if isopen:
            if self.answers[-1] == n:
                if self.items[self.answers[-1]][2] == self.items[answers[-2]][2]:
                    pass #すでに正解しているときは、閉じない
                else:
                    self.canvas.tag_raise(a[1])
                    self.answers.pop()
        else:
            self.canvas.tag_lower(a[1])
            self.answers.append(n)
            if len(self.answers) % 2 == 0:
                if self.items[self.answers[-1]][2] == self.items[self.answers[-2]][2]:
                    # 同じ数字が選択されました。
                    if len(self.answers) == len(self.items):
                        message_string = ("トレーニング終了です。\n" 
                                          "問題数:" + str(N) + "\n"
                                          "タップ数:" + str(self.tapped) + "\n\n"
                                          "もう一度、やりますか?")
                        if messagebox.askyesno("Congraturation!", message_string):
                            self.refresh_cards()
                        else:
                            self.destroy()
                else:
                    self.after(500, self.close_card) # 違う数字が選択されました。
                    self.closing = True

    def close_card(self):
        if len(self.answers) < 2:
            return
        self.canvas.tag_raise(self.items[self.answers[-1]][1])
        self.answers.pop()
        self.canvas.tag_raise(self.items[self.answers[-1]][1])
        self.answers.pop()
        self.closing = False
        
if __name__ == '__main__':
    numberMaching = NumberMaching()
    numberMaching.mainloop()

説明

 もっと簡単にできると思っていましたが、意外と作るのに時間がかかってしまいました。まず、カードのマークをどうしようかなぁ、ということで、グーグル先生に相談してみました。Unicodeにトランプのマークがあったので、それを利用することにしました。初期データは、namedtupleをつかって作ることにしました。7~14行目と22行目です。あと、カードをシャッフルするのは、randomモジュールからsampleをつかっています。35行目です。順番を入れ替えるだけならshuffleでも同じように動きます。sampleshuffleの違いは、sampleは新しいリストをつくるのに対して、shuffleは指定されたリストの順序を入れ替える、という点です。

 画面のカードを描画する部分は再実行したいときに、呼び出せるようにrefresh_cards()メソッドにまとめました。28~54行目です。キャンバス上にcreate_text()でカードのマークを描画して、その上からcreate_rectangle()で白い長方形を描画してカードを表現しました。これらの項目を作成するときに、"card"タグをセットして、クリックしたときに何らかの処理ができるよう、card_tapped()メソッドをバインドしました。

 めくったカードの正解、不正解については、めくられたカードを保存するようにリストを使うことにしました。37行目のself.answers = []がその宣言部分です。カードがめくられるたびに、このanswersappend()で追加していきますが、追加したあとの判定で、めくった枚数が奇数のときはなにもしない、偶数のときは二枚目がめくられたということで、チェックするようにしました。

 チェックは、rankが同じなら数値が一致したということで何もしないのですが、不一致の場合は、カードを元通りにもどす、ということをやるようにしました。すべてめくられたら、終了メッセージを出力しています。

まとめ

 もっと簡単にできるのかと思っていましたが、意外と時間がかかってしまった原因は、カードの位置決め部分に汎用性を持たせて、N=6以外にも実行できるようにしたため、ちょっと難しくなってしまった、というところが1点目ですね。あと、当たり判定するためのデータ構造をどうしようか、と、迷いながら進めていたというところが2点目ですね。先に仕様を固めてからつくればよかったですね。小さいプログラムだからと油断し過ぎました。

 作業記憶と注意力、鍛えましょう!!!

コメントを残す

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


reCaptcha の認証期間が終了しました。ページを再読み込みしてください。